﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Elderos.Ontology;
using Elderos.Utils.Logging;

namespace Elderos.Highlight.Search
{
    public class PeopleSearcher : ISearchStrategy
    {
        private readonly IOntologyData _ontology;

        private HashSet<string> _names;

        private readonly object _initLocker = new object();

        private StringComparer _stringComparer = StringComparer.InvariantCultureIgnoreCase;
        public StringComparer StringComparer
        {
            get { return _stringComparer; }
            set { _stringComparer = value; }
        }

        private Dictionary<string, string> _nameSynonyms;

        /// <summary>
        /// Key - lastname
        /// </summary>
        private Dictionary<string, List<PersonName>> _lastNameDict;

        private Dictionary<PersonName, List<int>> _correspondence; 

        public PeopleSearcher(IOntologyData ontology)
        {
            Logger.Assert<ArgumentNullException>(() => ontology != null, "Ontology cannot be null");
            _ontology = ontology;
        }

        public void Initialize()
        {
            if (IsInitialized) return;
            lock (_initLocker)
            {
                if (IsInitialized) return;

                InitializeNameSynonyms();

                InitializePersonNames();

                IsInitialized = true;
            }
        }

        private void InitializePersonNames()
        {
            _names = new HashSet<string>(StringComparer);

            _lastNameDict = new Dictionary<string, List<PersonName>>(StringComparer);
            _correspondence = new Dictionary<PersonName, List<int>>();

            foreach (var synonym in _ontology.GetFBTypeSynonyms(new[] {"/people/person"}))
            {
                PersonName personName;
                if (!TryParseSynonym(synonym, out personName)) continue;

                if (_correspondence.ContainsKey(personName))
                    _correspondence[personName].Add(synonym.EntityID);
                else
                    _correspondence.Add(personName, new List<int>(1) {synonym.EntityID});

                string lastname = personName.LastName;

                if (_lastNameDict.ContainsKey(lastname))
                    _lastNameDict[lastname].Add(personName);
                else
                    _lastNameDict.Add(lastname, new List<PersonName>(1) {personName});

                _names.Add(personName.Name);
            }

            foreach (var kv in _lastNameDict)
            {
                kv.Value.TrimExcess();
            }
        }

        private void InitializeNameSynonyms()
        {
            _nameSynonyms = new Dictionary<string, string>(StringComparer);
            foreach (var nameSynonym in _ontology.GetNameSynonyms())
            {
                string normalized = nameSynonym.Normalized;
                if (_nameSynonyms.ContainsKey(normalized))
                    normalized = _nameSynonyms[normalized];

                //this is required to avoid situations when main synonym has its own main synonym
                if (!_nameSynonyms.ContainsKey(normalized))
                    _nameSynonyms.Add(normalized, normalized);

                if (!_nameSynonyms.ContainsKey(nameSynonym.Name))
                    _nameSynonyms.Add(nameSynonym.Name, normalized);

            }
        }

        private bool TryParseSynonym(SynonymEntityDTO synonym, out PersonName personName)
        {
            var syno = synonym.Synonym;
            string[] parts = syno.Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
            if (parts.Length < 2)
            {
                personName = default(PersonName);
                return false;
            }

            string name = string.Intern(parts.First().ToLowerInvariant());
            string lastName = string.Intern(parts.Last().ToLowerInvariant());

            if (_nameSynonyms.ContainsKey(name))
                name = _nameSynonyms[name];

            personName = new PersonName(name, lastName);
            return true;
        }

        public bool IsInitialized { get; private set; }
        
        private string NormalizeName(string name)
        {
            if (_nameSynonyms.ContainsKey(name))
                return _nameSynonyms[name];
            return name;
        }

        public List<Position> Search(PreparedText prepared)
        {
            Logger.Assert<InvalidOperationException>(() => IsInitialized, "Strategy is not initialized");

            var positions = SearchFullName(prepared);

            return positions;
        }

        private List<Position> SearchFullName(PreparedText prepared)
        {
            var tokens = prepared.Tokens;
            var positions = new List<Position>();

            for (int i = 1; i < tokens.Length; i++)
            {
                string lastname = tokens[i];
                if (!_lastNameDict.ContainsKey(lastname)) continue;

                List<PersonName> pretenders = _lastNameDict[lastname];

                for (int j = i - 1; j >= 0 && i - j < 10; j--)
                {
                    var token = tokens[j];

                    //ignoring "Homer J[.] Simpson"
                    if (token.IsPunctuation) continue;

                    if (token.IsWord)
                    {
                        string name = NormalizeName(token);
                        var personName = new PersonName(name, lastname);

                        //for "H. Simpson"
                        if (token.Length == 1 && char.IsUpper(token[0]))
                        {
                            foreach (var pretender in pretenders)
                            {
                                if (char.ToUpper(pretender.Name[0]) != token[0]) continue;

                                int start = tokens[j].First;
                                int length = tokens[i].Last - tokens[j].First + 1;
                                var entityIDs = _correspondence[personName];
                                string synonym = prepared.Text.Substring(start, length);
                                positions.Add(new Position(start, length, entityIDs, synonym));
                            }
                        }
                        //for "Homer Simpson"
                        else
                        {
                            foreach (var pretender in pretenders)
                            {
                                if (pretender.Name != token) continue;

                                int start = tokens[j].First;
                                int length = tokens[i].Last - tokens[j].First + 1;
                                var entityIDs = _correspondence[personName];
                                string synonym = prepared.Text.Substring(start, length);
                                positions.Add(new Position(start, length, entityIDs, synonym));
                            }
                        }
                        

                        //ignoring "Ostap [Suleiman Bertha Maria] Bender"
                        if (_names.Contains(token)) continue;

                        //ignoring "Homer [J]. Simpson"
                        if (token.Length == 1 && char.IsUpper(token[0])) continue;

                        //ignoring "Marco [di] Biaggio"
                        if (_ontology.PreNames.Contains(token)) continue;

                        break;
                    }
                    else break;
                }
            }

            return positions;
        }

        public event InitializationProgressHandler InitializationProgress;

        private struct PersonName
        {
            public PersonName(string name, string lastName)
            {
                Name = name;
                LastName = lastName;
            }

            public readonly string Name;
            public readonly string LastName;

            public override bool Equals(object obj)
            {
                if (!(obj is PersonName)) return false;
                var other = (PersonName) obj;

                var comparer = StringComparer.CurrentCultureIgnoreCase;

                return comparer.Compare(this.Name, other.Name) == 0 && comparer.Compare(this.LastName, other.LastName) == 0;
            }

            public override int GetHashCode()
            {
                return Name.ToLowerInvariant().GetHashCode() ^ LastName.ToLowerInvariant().GetHashCode();
            }
        }
    }
}
